/**************************************************************************
* This file is part of Hawkengine.
*
* Hawkengine is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Hawkengine is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Hawkengine.  If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/


/**************************************************************************
* Dependencies
**************************************************************************/
#include "engine/animation.h"
#include "engine/util.h"
 
/**************************************************************************
* Preprocessing
**************************************************************************/

/**************************************************************************
* Types
**************************************************************************/
typedef enum
{
  INSTRUCTION_CMD_FLOAT_APPROACH,
  INSTRUCTION_CMD_FLOAT_SET,
  INSTRUCTION_CMD_INT_SET,
  INSTRUCTION_CMD_BOOL_TOGGLE,
  INSTRUCTION_CMD_COUNT,
} instruction_cmd_type;

typedef struct
{
  instruction_cmd_type instruction_cmd;
  void* value;
  float delay;
  union {
    struct {
      float start;
      float goal;
      float time;
      float elapsed;
    } f_approach;
    struct {
      float goal;
    } f_set;
    struct {
      int goal;
    } i_set;
  } u;
  BOOL background;
} instruction_data_type;

struct instruction_node;
typedef struct instruction_node instruction_node_type;
struct instruction_node
{
  instruction_data_type task;
  instruction_node_type* next;
  instruction_node_type* prev;
};

/**************************************************************************
* Variables
**************************************************************************/

/**************************************************************************
* Static Variables
**************************************************************************/
static const int INITIAL_INSTRUCTION_NODES = 16;
static instruction_node_type* active_instructions;
static instruction_node_type* pending_instructions;
static instruction_node_type* unused_instructions;

/**************************************************************************
* Function Prototypes
**************************************************************************/
static void add(float* value, float goal, float secs, float delay, BOOL background);
static void add_node(instruction_node_type** node_list, instruction_node_type* node);
static float approach(float start, float end, float portion);
static void create_unused_instruction_node(void);
static instruction_node_type* get_unused_instruction_node(void);
static void remove_node(instruction_node_type** node_list, instruction_node_type* node);

/**************************************************************************
* Function Implementations
**************************************************************************/
static void add(float* value, float goal, float secs, float delay, BOOL background)
{
  instruction_node_type* instruction_node;
 
  if (delay > 0) {
    // start after delay
    instruction_node = get_unused_instruction_node();

    instruction_node->task.instruction_cmd = INSTRUCTION_CMD_FLOAT_APPROACH;
    instruction_node->task.value = value;
    instruction_node->task.delay = delay;
    instruction_node->task.u.f_approach.goal = goal;
    instruction_node->task.u.f_approach.time = secs;
    instruction_node->task.u.f_approach.elapsed = 0;

    // add to pending instructions
    if (pending_instructions) {
      pending_instructions->prev = instruction_node;
    }
    instruction_node->next = pending_instructions;
    pending_instructions = instruction_node;
  } else {
    // start right away
    // see if we're animating this float already
    instruction_node = active_instructions;
    while (instruction_node) {
      if (instruction_node->task.value == value) {
        break;
      }
      instruction_node = instruction_node->next;
    }

    if (!instruction_node) {
      // no existing node
      if ((*value) == goal) {
        return;
      }

      // get task and add add to active instructions
      instruction_node = get_unused_instruction_node();
      if (active_instructions) {
        active_instructions->prev = instruction_node;
      }
      instruction_node->next = active_instructions;
      active_instructions = instruction_node;

      instruction_node->task.instruction_cmd = INSTRUCTION_CMD_FLOAT_APPROACH;
      instruction_node->task.value = value;
    }

    // set parameters whether this is new or not
    instruction_node->task.delay = 0;
    instruction_node->task.u.f_approach.start = *((float*)(instruction_node->task.value));
    instruction_node->task.u.f_approach.goal = goal;
    instruction_node->task.u.f_approach.time = secs;
    instruction_node->task.u.f_approach.elapsed = 0;
    instruction_node->task.background = background;
  }
}

static void add_node(instruction_node_type** node_list, instruction_node_type* node)
{
  node->next = (*node_list);
  if (*node_list) {
    // other nodes exist, connect them back to new node
    node->prev = (*node_list)->prev;
    (*node_list)->prev = node;
  }

  // update destination node with given node
  (*node_list) = node;
}

static float approach(float start, float end, float portion)
{
  // approach the end from start with portion in [0, 1]
  return change_scale(portion, 0, 1, start, end);
}


void animation_add(float* value, float goal, float secs)
{
  add(value, goal, secs, 0, FALSE);
}

void animation_add_background(float* value, float goal, float secs)
{
  add(value, goal, secs, 0, TRUE);
}

void animation_add_background_with_delay(float* value, float goal, float secs, float delay)
{
  add(value, goal, secs, delay, TRUE);
}

void animation_add_with_delay(float* value, float goal, float secs, float delay)
{
  add(value, goal, secs, delay, FALSE);
}

void animation_cancel(void* value)
{
  instruction_node_type* instruction_node;

  // check active instructions
  instruction_node = active_instructions;
  while (instruction_node) {
    if (instruction_node->task.value == value) {
      break;
    }
    instruction_node = instruction_node->next;
  }
  if (instruction_node) {
    // move the task node into the unused nodes
    remove_node(&active_instructions, instruction_node);
    add_node(&unused_instructions, instruction_node);
    return;
  }
 
  // check pending instructions
  instruction_node = pending_instructions;
  while (instruction_node) {
    if (instruction_node->task.value == value) {
      break;
    }
    instruction_node = instruction_node->next;
  }
  if (instruction_node) {
    // move the task node into the unused nodes
    remove_node(&pending_instructions, instruction_node);
    add_node(&unused_instructions, instruction_node);
    return;
  }
}

void animation_finish(void)
{
  static const int MAX_ATTEMPTS = 4;
  int attempt = 0;
  while (animation_pending()) {
    animation_update(99999);

    attempt++;
    if (attempt >= MAX_ATTEMPTS) {
      break;
    }
  }
}

void animation_init(void)
{
  int i;
 
  active_instructions = NULL;
  pending_instructions = NULL;
  unused_instructions = NULL;
 
  // create some initial task nodes
  for (i = 0; i < INITIAL_INSTRUCTION_NODES; i++) {
    create_unused_instruction_node();
  }
}

BOOL animation_pending(void)
{
  instruction_node_type* node;
 
  node = active_instructions;
  while (node) {
    if (!(node->task.background)) {
      return TRUE;
    }
 
    node = node->next;
  }
 
  node = pending_instructions;
  while (node) {
    if (!(node->task.background)) {
      return TRUE;
    }
 
    node = node->next;
  }
 
  return FALSE;
}

void animation_set_float(float *value, float goal, float delay)
{
  instruction_node_type* instruction_node;

  // no existing node get new node and add to list
  instruction_node = get_unused_instruction_node();

  instruction_node->task.instruction_cmd = INSTRUCTION_CMD_FLOAT_SET;
  instruction_node->task.value = value;
  instruction_node->task.delay = delay;
  instruction_node->task.u.f_set.goal = goal;
 
  // add to pending instructions
  if (pending_instructions) {
    pending_instructions->prev = instruction_node;
  }
  instruction_node->next = pending_instructions;
  pending_instructions = instruction_node;
}

void animation_set_int(int* value, int goal, float delay)
{
  instruction_node_type* instruction_node;

  // no existing node get new node and add to list
  instruction_node = get_unused_instruction_node();

  instruction_node->task.instruction_cmd = INSTRUCTION_CMD_INT_SET;
  instruction_node->task.value = value;
  instruction_node->task.delay = delay;
  instruction_node->task.u.i_set.goal = goal;
 
  // add to pending instructions
  if (pending_instructions) {
    pending_instructions->prev = instruction_node;
  }
  instruction_node->next = pending_instructions;
  pending_instructions = instruction_node;
}

void animation_toggle_bool(BOOL* value, float delay)
{
  instruction_node_type* instruction_node;

  // no existing node get new node and add to list
  instruction_node = get_unused_instruction_node();

  instruction_node->task.instruction_cmd = INSTRUCTION_CMD_BOOL_TOGGLE;
  instruction_node->task.value = value;
  instruction_node->task.delay = delay;
 
  // add to pending instructions
  if (pending_instructions) {
    pending_instructions->prev = instruction_node;
  }
  instruction_node->next = pending_instructions;
  pending_instructions = instruction_node;
}

void animation_uninit(void)
{
  instruction_node_type* node;

  while (active_instructions) {
    node = active_instructions;
    active_instructions = active_instructions->next;
    free(node);
  }
  active_instructions = NULL;
 
  while (pending_instructions) {
    node = pending_instructions;
    pending_instructions = pending_instructions->next;
    free(node);
  }
  active_instructions = NULL;

  while (unused_instructions) {
    node = unused_instructions;
    unused_instructions = unused_instructions->next;
    free(node);
  }
  unused_instructions = NULL;
}

void animation_update(float secs)
{
  instruction_node_type* instruction_node;
  instruction_node_type* curr_instruction_node;
  instruction_data_type* curr_task;
 
  if (secs <= 0) {
    return;
  }

  // handle all pending animation instructions
  instruction_node = pending_instructions;
  while (instruction_node) {
    curr_instruction_node = instruction_node;
    curr_task = &(curr_instruction_node->task);
    instruction_node = instruction_node->next;

    // handle delays
    curr_task->delay -= secs;
    if (curr_task->delay > 0) {
      // still more waiting to do
      continue;
    }
 
    curr_task->delay = 0;
    switch (curr_task->instruction_cmd) {
    case INSTRUCTION_CMD_FLOAT_APPROACH:
      animation_add(curr_task->value, curr_task->u.f_approach.goal,
                    curr_task->u.f_approach.time);
      break;
    case INSTRUCTION_CMD_FLOAT_SET:
      *((float*)(curr_task->value)) = curr_task->u.f_set.goal;
      break;
    case INSTRUCTION_CMD_INT_SET:
      *((int*)(curr_task->value)) = curr_task->u.i_set.goal;
      break;
    case INSTRUCTION_CMD_BOOL_TOGGLE:
      _TOGGLE(*((BOOL*)(curr_task->value)));
      break;
    default:
      break;
    }
 
    // task handled, clear out
    remove_node(&pending_instructions, curr_instruction_node);
    add_node(&unused_instructions, curr_instruction_node);
  }

  // handle active animation instructions
  instruction_node = active_instructions;
  while (instruction_node) {
    curr_instruction_node = instruction_node;
    curr_task = &(curr_instruction_node->task);
    instruction_node = instruction_node->next;

    if (curr_task->instruction_cmd != INSTRUCTION_CMD_FLOAT_APPROACH) {
      // invalid active task
      remove_node(&active_instructions, curr_instruction_node);
      add_node(&unused_instructions, curr_instruction_node);
      continue;
    }

    // update the time
    curr_task->u.f_approach.elapsed += secs;
    if ((curr_task->u.f_approach.elapsed >= curr_task->u.f_approach.time) ||
        (*((float*)(curr_task->value)) == curr_task->u.f_approach.goal)) {
      // this animation is done
      *((float*)(curr_task->value)) = curr_task->u.f_approach.goal;
 
      // move the task node into the unused nodes
      remove_node(&active_instructions, curr_instruction_node);
      add_node(&unused_instructions, curr_instruction_node);
    } else {
      // update and keep animation
      *((float*)(curr_task->value)) = approach(curr_task->u.f_approach.start,
                                               curr_task->u.f_approach.goal,
                                               (curr_task->u.f_approach.elapsed /
                                                curr_task->u.f_approach.time));
    }
  }
}

static void create_unused_instruction_node(void)
{
  instruction_node_type* node;
  node = malloc(sizeof(instruction_node_type));
  memset(node, 0, sizeof(instruction_node_type));
  add_node(&unused_instructions, node);
}

static instruction_node_type* get_unused_instruction_node(void)
{
  instruction_node_type* node;

  if (!unused_instructions) {
    // no more nodes, create one
    create_unused_instruction_node();
  }
  node = unused_instructions;
  remove_node(&unused_instructions, node);
 
  // initialize the node
  memset(node, 0, sizeof(instruction_node_type));
 
  return node;
}

static void remove_node(instruction_node_type** node_list, instruction_node_type* node)
{
  if ((*node_list) == node) {
    // head of the list, update the node_list pointer
    (*node_list) = node->next;
  }
  if (node->prev) {
    node->prev->next = node->next;
  }
  if (node->next) {
    node->next->prev = node->prev;
  }
  node->next = node->prev = NULL;
}
